.com
Hosted by:
Unit testing expertise at your fingertips!
Home | Discuss | Lists

Test Utility Method

The book has now been published and the content of this chapter has likely changed substanstially.
Please see page 599 of xUnit Test Patterns for the latest information.
How do we reduce Test Code Duplication?

We encapsulate the test logic we want to reuse behind a suitably named utility method.

Sketch Test Utility Method embedded from Test Utility Method.gif

As we write tests, you will invariably find ourselves needing to repeat the same logic in many many tests. Initially, we will just "clone & twiddle" as we write additional tests that need the same logic. Sooner or later, we come to the realization that all the Test Code Duplication (page X) is starting to cause us problems. This is a good time to think about introducing a Test Utility Method.

How It Works

The subroutine and function were some of the earliest ways devised to reuse logic in several places within a program. Test Utility Method is just the same principle applied to object-oriented test code. We move any logic we find appearing in more than one test into a Test Utility Method that we can then call from various tests or even several times from within a single test. Of course, we will want to pass in anything that varies from usage to usage as arguments to the Test Utility Method.

When To Use It

We should use a Test Utility Method whenever test logic appears in several tests and we want to be able to reuse it. Another reason for using a Test Utility Method is because we want to be very sure that the logic works as expected. The best way to achieve that is to write Self-Checking Tests (see Goals of Test Automation on page X) (unit tests) for the logic. Because the Test Methods (page X) cannot easily be tested, it is best to do this by removing the logic from the test methods into Test Utility Methods where it can be more easily tested.

The main drawback of using Test Utility Methods is that it creates another API that the test automaters must build and understand. This can be largely mitigated through the use of Intent Revealing Names[SBPP] for the Test Utility Methods and through the use of refactoring as the means for defining the Test Utility Methods.

There are as many different kinds of Test Utility Methods as there are kinds of logic in a Test Method. The following is a brief summary of some of the most popular kinds. Some of these are important enough to warrant their own pattern write-ups in the corresponding section of this book:

Variation: Creation Method

Creation Methods (page X) are used to create read to use objects as part of fixture setup. They hide the complexity of object creation and interdependencies from the test. Creation Method has enough variants to warrant writing it up separately.

Variation: Attachment Method

Attachment Method (see Creation Method) are a special form of Creation Method used to amend already-created objects as part of fixture setup.

Variation: Finder Method

We encapsulate any logic required to retrieve objects from a Shared Fixture (page X) behind function that returns the object(s). We give the function an Intent Revealing Name so that anyone reading the test can easily understand the fixture we are using in this test.

We should use a Finder Method any time we need to find an existing Shared Fixture object that meets some criteria and we want to avoid a Fragile Fixture (see Fragile Test on page X) and High Test Maintenance Cost (page X). Finder Methods can be used in either a pure Shared Fixture strategy or a hybrid strategy such as Immutable Shared Fixture (see Shared Fixture). Finder Methods also help prevent Obscure Tests (page X) by encapsulating the mechanism of how the required objects are found and exactly which objects to use thus helping the reader focus on understanding why a particular object is being used and how that relates to the expected outcome described in the assertions. This helps us move toward Tests as Documentation (see Goals of Test Automation).

Most Finder Methods return a single object reference but that object may be the root of a tree of objects (e.g. an invoice might refer to the customer and various addresses as well as containing a list of line items.) In some circumstances, we may choose to define a Finder Method that returns a collection (Array or Hash) of objects, but the use of this type of Finder Method is less common. Finder Methods may also use update parameters to pass additional objects back to the test that called them, but this is not as intent revealing as using a function. I do not recommend initialization of instance variables as a way of passing back objects because it is obscure and keeps us from moving the Finder Method to a Test Helper (page X) later.

The Finder Method can find objects in the Shared Fixture either by using direct references (instance variables or class variables initialized in the fixture setup logic), by looking them up by known keys or it can search for the objects using criteria. Using direct references or known keys has the advantage of always returning exactly the same object each time the test is been run. The main drawback is that some other test may have modified the object such that it may no longer match the criteria implied by the Finder Method's name. Searching by criteria can avoid this problem but the tests may take longer to run and could be less deterministic because they may be using using different objects each time they are run. Either way, we have fewer places to modify whenever the Shared Fixture is modified (compared to when they are used directly within the Test Method.)

Variation: SUT Encapsulation Method


Also known as: SUT API Encapsulation

Another reason for using a self is to encapsulate unnecessary knowledge of the API of the system under test (SUT). What constitutes unnecessary? Any method we call on the SUT that is not the method being tested creates additional coupling between the test and the SUT. Creation Methods and Custom Assertions (page X) are common enough to warrant their own write-ups. Here, I'll focus on the less common uses. For example, if the method we are exercising or (which we use for verifying the outcome) has a complicated signature, we increase the amount of work involved to write and maintain the test code and may also make it harder to understand the tests (Obscure Test.) We can avoid this by wrapping these calls in SUT Encapsulation Methods that are intent revealing and which may have a simpler signature.

Variation: Custom Assertion

Custom Assertion are used to specify test-specific equality in a way that is reusable across many tests. They hide the complexity of comparing the expected outcome with the actual outcome. Custom Assertions are typically free of side effects in that they do not interact with the SUT to retrieve the outcome; that is left to the caller.

Variation: Verification Method

Verification Method (see Custom Assertion) are used to verify that the expected outcome has occurred. They hide the complexity of verifying the outcome from the test. Unlike Custom Assertions, they interact with the SUT.

Variation: Parameterized Test

The most complete form of Test Utility Method is the Parameterized Test (page X). It is in essense an almost complete test that can be reused in many circumstances. We simply provide what varies from test to test as a parameter and let it execute all the stages of the Four-Phase Test (page X) for us.

Variation: Cleanup Method

Cleanup Methods(One could call this a "Teardown Method" but that would be confused with the method used in Implicit Teardown (page X).) are used during the fixture teardown phase of the test to clean up any resources that might stay allocated after the test ends. Refer to the pattern Automated Teardown (page X) for a more detailed discussion and examples.

Implementation Notes

The main objection some people have to using Test Utility Methods is that it removes some of the logic from the test and that may make it harder to read. One way we can avoid this when using Test Utility Methods is to use Intent Revealing Names for the Test Utility Methods. In fact, well-chosen names can make the tests even easier to understand because they help prevent Obscure Test. It is also useful to keep the Test Utility Methods relatively small and self-contained. We can achieve this by passing all arguments to them explicitly as parameters rather than using instance variables and by returning any objects that the tests will require as explicit return values or updates parameters.

To ensure that the Test Utility Methods have Intent Revealing Names, we should let the tests pull the Test Utility Methods into existence rather than just inventing Test Utility Methods that we think may be needed later. This "outside in" approach to writing code avoids "borrowing tomorrow's trouble" and helps us find the minimal solution.

Writing the reusable Test Utility Method is pretty straight-forward. The bigger question is where we would put it. If the Test Utility Method is only needed in Test Methods in a single Testcase Class (page X) then we can put the Test Utility Method onto that class but if we need it from several classes, the solution is a bit more complicated. It all comes down to type visibility. The client classes need to be able to see the Test Utility Method and the Test Utility Method needs to be able to see all the types and classes it depends on. When it doesn't depend on many or when everything it depends on is visible from a single place, the Test Utility Method can be put into a common Testcase Superclass (page X) we define for our project or company. If it depends on types/classes that cannot be seen from a single place that all the clients can see, it may be necessary to put it on a Test Helper in the appropriate test package or subsystem. In larger systems with many groups of domain objects, it is common to have one Test Helper for each group (package) of related domain objects.

Variation: Test Utility Test

One major advantage to using Test Utility Methods is that code that would otherwise be Untestable Test Code (see Hard to Test Code on page X) can now be tested with Self-Checking Tests. A good example of this is a Custom Assertion Test (see Custom Assertion).

Motivating Example

The following is a test as many novice test automaters would first write it.

   public void testAddItemQuantity_severalQuantity_v1(){
      Address billingAddress = null;
      Address shippingAddress = null;
      Customer customer = null;
      Product product = null;
      Invoice invoice = null;
      try {
         //   Setup Fixture
         billingAddress = new Address("1222 1st St SW", "Calgary", "Alberta",
                                      "T2N 2V2", "Canada");
         shippingAddress = new Address("1333 1st St SW", "Calgary", "Alberta",
                                       "T2N 2V2", "Canada");
         customer = new Customer( 99, "John", "Doe", new BigDecimal("30"),
                                  billingAddress,
                                  shippingAddress);
         product = new Product( 88, "SomeWidget", new BigDecimal("19.99"));
         invoice = new Invoice( customer );
         // Exercise SUT
         invoice.addItemQuantity( product, 5 );
         // Verify Outcome
         List lineItems = invoice.getLineItems();
         if (lineItems.size() == 1) {
            LineItem actItem = (LineItem) lineItems.get(0);
            assertEquals("inv", invoice, actItem.getInv());
            assertEquals("prod", product, actItem.getProd());
            assertEquals("quant", 5, actItem.getQuantity());
            assertEquals("discount", new BigDecimal("30"),
                         actItem.getPercentDiscount());
            assertEquals("unit price", new BigDecimal("19.99"),
                         actItem.getUnitPrice());
            assertEquals("extended", new BigDecimal("69.96"),
                         actItem.getExtendedPrice());
         } else {
            assertTrue("Invoice should have 1 item", false);
         }
      } finally {
         // Teardown
         deleteObject(invoice);
         deleteObject(product);
         deleteObject(customer);
         deleteObject(billingAddress);
         deleteObject(shippingAddress);
      }
   }
Example TestUtilityMethodsNone embedded from java/com/clrstream/camug/example/test/TestUtiltyMethodExample.java

This test is difficult to understand because it exhibits many code smells including Obscure Test and Hard-Coded Test Data (see Obscure Test).

Refactoring Notes

Test Utility Methods are often created by mining tests for reusable logic when writing other tests. We can use an Extract Method[Fowler] refactoring to pull the code for the Test Utility Method out of one Test Method and put it onto the Testcase Class as a Test Utility Method. From there, we may choose to move it to a superclass by using a Pull Up Method[Fowler] refactoring or to another class by using a Move Method[Fowler] refactoring.

Example: Test Utility Method

Here's the refactored versions of the test. Note how much simpler this test is to understand than what we started with. This is just an example of what we can achieve by using Test Utility Methods!

   public void testAddItemQuantity_severalQuantity_v13(){
      final int QUANTITY = 5;
      final BigDecimal CUSTOMER_DISCOUNT = new BigDecimal("30");
      //   Setup Fixture
      Customer customer = findActiveCustomerWithDiscount(CUSTOMER_DISCOUNT);
      Product product = findCurrentProductWith3DigitPrice( );
      Invoice invoice = createInvoice(customer);
      // Exercise SUT
      invoice.addItemQuantity(product, QUANTITY);
      // Verify Outcome    
      final BigDecimal BASE_PRICE = product.getUnitPrice().
      multiply(new BigDecimal(QUANTITY));
      final BigDecimal EXTENDED_PRICE = BASE_PRICE.subtract(BASE_PRICE.multiply( CUSTOMER_DISCOUNT.movePointLeft(2)));
      LineItem expected = createLineItem( QUANTITY, CUSTOMER_DISCOUNT, EXTENDED_PRICE, product, invoice);
      assertContainsExactlyOneLineItem(invoice, expected);
   }
Example TestUtilityMethodsGalore embedded from java/com/clrstream/camug/example/test/TestUtiltyMethodExample.java

Let's go through the changes step by step. First,we replaced the code to create the Customer and Product with calls to Finder Methods that retrieve them from an Immutable Shared Fixture because we don't plan to change these objects.

   protected Customer findActiveCustomerWithDiscount( BigDecimal percentDiscount) {
      return CustomerHome.findCustomerById( ACTIVE_CUSTOMER_WITH_30PC_DISCOUNT_ID);
   }
Example TestUtilityMethodFinder embedded from java/com/clrstream/camug/example/test/TestUtiltyMethodExample.java

Next, we introduced a Creation Method for the Invoice to which we plan to add the LineItem.

   protected Invoice createInvoice(Customer customer) {
      Invoice newInvoice = new Invoice(customer);
      registerTestObject(newInvoice);
      return newInvoice;
   }
  
   List testObjects;
   protected void registerTestObject(Object testObject) {
      testObjects.add(testObject);       
   }
Example TestUtilityMethodCreation embedded from java/com/clrstream/camug/example/test/TestUtiltyMethodExample.java

To avoid the need for Inline Teardown (page X), we've registered each of the objects we created with our Automated Teardown mechanism which we call from the tearDown method.

   private void deleteTestObjects() {
      Iterator i = testObjects.iterator();
      while (i.hasNext()) {
         try {
            deleteObject(i.next());
         } catch (RuntimeException e) {
            // nothing to do; we just want to make sure
            // we continue on to the next object in the list.}
      }
   }
  
   public void tearDown() {
      deleteTestObjects();
   }
Example TestUtilityMethodCleanup embedded from java/com/clrstream/camug/example/test/TestUtiltyMethodExample.java

Finally, we extracted a Custom Assertion to verify that the right LineItem has been added to the Invoice.

   void assertContainsExactlyOneLineItem( Invoice invoice, LineItem expected) {
      List lineItems = invoice.getLineItems();
      assertEquals("number of items", lineItems.size(), 1);
      LineItem actItem = (LineItem)lineItems.get(0);
      assertLineItemsEqual("",expected, actItem);
   }
Example TestUtilityMethodCustomAssertion embedded from java/com/clrstream/camug/example/test/TestUtiltyMethodExample.java


Page generated at Wed Feb 09 16:39:44 +1100 2011

Copyright © 2003-2008 Gerard Meszaros all rights reserved

All Categories
Introductory Narratives
Web Site Instructions
Code Refactorings
Database Patterns
DfT Patterns
External Patterns
Fixture Setup Patterns
Fixture Teardown Patterns
Front Matter
Glossary
Misc
References
Result Verification Patterns
Sidebars
Terminology
Test Double Patterns
Test Organization
Test Refactorings
Test Smells
Test Strategy
Tools
Value Patterns
XUnit Basics
xUnit Members
All "Test Organization"
Named Test Suite
Test Code Reuse:
--Test Utility Method
----Finder Method
----SUT Encapsulation Method
----SUT API Encapsulation
----Cleanup Method
----Test Utility Test
--Parameterized Test
Testcase Class Structure:
--Testcase Class per Feature
--Testcase Class per Fixture
--Testcase Class per Class
Utility Method Location:
--Test Helper
--Testcase Superclass